/*
 * Copyright 2006-2007 S W McIlroy
 * Licensed  under the  Apache License,  Version 2.0  (the "License");
 * you may not use  this file  except in  compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed  under the  License is distributed on an "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mockfu;

import static org.mockfu.Exceptions.*;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.mockfu.Mocks.ArrayEqMatcher;
import org.mockfu.Mocks.EqMatcher;

abstract class Constraints
{
    private static ThreadLocal<LinkedList<Constraint>> currentConstraints = new ThreadLocal<LinkedList<Constraint>>();
    private static Map<Class<?>, Object> magicHat = new HashMap<Class<?>, Object>();
    private static ThreadLocal<ConstrainedMethod> currentConstrainedMethod = new ThreadLocal<ConstrainedMethod>();

    static
    {
        addMagicInstances( new Boolean( false ), new Byte( (byte) -117 ),
                           new Short( (short) -3291 ), new Integer( -83209775 ),
                           new Long( -48003506172133615L ), new Float( -80290.7110934882f ),
                           new Double( -203972.991468517322 ), new String(), new Object() );
        /* add primitive types */
        magicHat.put( boolean.class, false );
        magicHat.put( byte.class, (byte) -117 );
        magicHat.put( short.class, (short) -3291 );
        magicHat.put( int.class, -83209775 );
        magicHat.put( long.class, -48003506172133615L );
        magicHat.put( float.class, -80290.7110934882f );
        magicHat.put( double.class, -203972.991468517322 );
    }

    private static void addMagicInstances( Object... instances )
    {
        for( Object instance : instances )
            magicHat.put( instance.getClass(), instance );
    }

    static <T> T registerConstraint( Class<T> klass, Matcher<?> matcher )
    {
        T key = getArgumentKey( klass );
        current().addLast( new Constraint( key, matcher ) );
        return key;
    }

    static <T> T registerConstraint( T argument, Matcher<?> matcher )
    {
        T key = getArgumentKey( argument );
        current().addLast( new Constraint( key, matcher ) );
        return key;
    }

    static LinkedList<Constraint> current()
    {
        LinkedList<Constraint> list = currentConstraints.get();
        if( list == null )
        {
            list = new LinkedList<Constraint>();
            currentConstraints.set( list );
        }
        return list;
    }

    static ConstrainedMethod currentMethod()
    {
        ConstrainedMethod constrainedMethod = currentConstrainedMethod.get();
        currentConstrainedMethod.set( null );
        return constrainedMethod;
    }

    static void setCurrentMethod(ConstrainedMethod method)
    {
        currentConstrainedMethod.set( method );
    }

    private static <T> T getArgumentKey( T argument )
    {
        if( argument == null )
            return null;
        T instance = getArgumentKey( argumentType( argument ) );
        if( instance != null )
            return instance;
        return argument;
    }

    static <T> Class<T> argumentType( T argument )
    {
        if( argument == null )
            return (Class<T>) Object.class;

        Class<T> type = (Class<T>) argument.getClass();
        String typeName = type.getSimpleName();
        if( typeName.contains( "$$Proxy$$" ) )
        {
            /*
             * if type is enhanced, look up base class in magic hat by constraint instance
             */
            for( Map.Entry<Class<?>, Object> entry : magicHat.entrySet() )
            {
                if( argument == entry.getValue() )
                    return (Class<T>) entry.getKey();
            }
            /* type is mocked or otherwise cglib generated, return base type */
            Class<?> superClass = type.getSuperclass();
            if (superClass != null && superClass != Object.class)
                return (Class<T>)superClass;
            for (Class<?> iface : type.getInterfaces())
                if (iface != Serializable.class)
                    return (Class<T>)iface;
            return (Class<T>) Object.class;
        }
        return type;
    }

    private static <T> T getArgumentKey( Class<T> klass )
    {
        T instance = (T) magicHat.get( klass );
        if( instance == null )
        {
            try
            {
                if( klass.isArray() )
                {
                    instance = (T) Array.newInstance( klass.getComponentType(), 0 );
                }
                else
                {
                    instance = ProxyBuilder.of( klass, new NullInterceptor() ).create();
                }
                magicHat.put( klass, instance );
            }
            catch( Throwable ex )
            {
                System.out.println( "Could not generate instance of " + klass + ": "
                        + ex.getMessage() );
            }
        }
        return instance;
    }

    private static class NullInterceptor implements MethodInterceptor
    {
        public Object intercept( InvocationContext context ) throws Throwable
        {
            return null;
        }
    }

    static List<Matcher<Object>> getMatchers( Object... arguments )
    {
        return getMatchers( true, arguments );
    }

    static List<Matcher<Object>> getMatchers( boolean defaultEq, Object... arguments )
    {
        try
        {
            return match( defaultEq, arguments, current(), new ArrayList<Constraint>() );
        }
        catch( Exception ex )
        {
            current().clear();
            throw undeclared( ex );
        }
    }

    private static LinkedList<Matcher<Object>> match( boolean defaultEq, Object[] arguments,
                                                      LinkedList<Constraint> constraints,
                                                      List<Constraint> matched )
    {
        LinkedList<Matcher<Object>> matchers = new LinkedList<Matcher<Object>>();

        for( int i = arguments.length - 1 ; i >= 0 ; i-- )
        {
            Object argument = arguments[i];

            if( !constraints.isEmpty() && constraints.getLast().isForArgument( argument ) )
            {
                Constraint constraint = constraints.removeLast();
                matched.add( constraint );
                matchers.addFirst( constraint.getMatcher() );
                continue;
            }

            if( argument != null && argument.getClass().isArray() )
            {
                Object[] objects = toObjectArray( argument );
                LinkedList<Matcher<Object>> arrayMatchers = match( defaultEq, objects, constraints,
                                                                   matched );
                ArrayEqMatcher matcher = new ArrayEqMatcher( arrayMatchers );
                matched.add( new Constraint( argument, matcher ) );
                matchers.addFirst( matcher );
            }
            else
            {
                for( Constraint next : matched )
                {
                    if( !(next.getMatcher() instanceof EqMatcher) && next.isForArgument( argument ) )
                        throw new IllegalStateException( "Ambiguous constraint, clarify using eq("
                                + toString( argument ) + ")" );
                }
                if( defaultEq )
                {
                    Mocks.EqMatcher matcher = new Mocks.EqMatcher( argument );
                    matched.add( new Constraint( argument, matcher ) );
                    matchers.addFirst( matcher );
                }
            }
        }
        return matchers;

    }

    private static Object[] toObjectArray( Object array )
    {
        if( array instanceof Object[] )
            return (Object[]) array;

        int length = Array.getLength( array );
        Object[] objects = new Object[length];

        for( int i = 0 ; i < length ; i++ )
            objects[i] = Array.get( array, i );

        return objects;
    }

    static String formatResult( Object actual, Matcher<Object> matcher )
    {
        if( actual == null )
            return "null";
        Map<Class<?>, ResultFormatter> formatters = new LinkedHashMap<Class<?>, ResultFormatter>();
        for( Matcher<Object> m : new MatcherIterator( matcher ) )
        {
            ResultFormatter formatter = m.getResultFormatter();
            if( formatter != null && !formatters.containsKey( formatter.getClass() ) )
                formatters.put( formatter.getClass(), formatter );
        }
        if( formatters.isEmpty() )
            return toString( actual );

        StringBuilder s = new StringBuilder();
        for( ResultFormatter formatter : formatters.values() )
        {
            if( s.length() != 0 )
                s.append( "; " );
            s.append( formatter.format( actual ) );
        }
        return toString( actual ) + " (" + s + ")";
    }

    static String toString( Object argument )
    {
        if( argument == null )
            return "null";

        boolean previousIncrementable = InvocationContext.setIncrementable( false );

        try
        {
            if( argument instanceof Boolean )
                return argument.toString();

            if( argument instanceof Number )
            {
                if( argument instanceof Byte )
                    return "(byte)" + argument;
                if( argument instanceof Short )
                    return "(short)" + argument;
                if( argument instanceof Long )
                    return argument + "L";
                if( argument instanceof Float )
                    return argument + "f";
                return argument.toString();
            }

            if( argument instanceof Collection )
            {
                StringBuilder s = new StringBuilder( "{" );
                for( Object each : (Iterable<Object>) argument )
                {
                    if( s.length() != 1 )
                        s.append( ", " );
                    s.append( toString( each ) );
                }
                return s.append( "}" ).toString();
            }

            if( argument.getClass().isArray() )
            {
                if( argument instanceof boolean[] )
                    return Arrays.toString( (boolean[]) argument );
                if( argument instanceof byte[] )
                    return Arrays.toString( (byte[]) argument );
                if( argument instanceof char[] )
                    return Arrays.toString( (char[]) argument );
                if( argument instanceof double[] )
                    return Arrays.toString( (double[]) argument );
                if( argument instanceof float[] )
                    return Arrays.toString( (float[]) argument );
                if( argument instanceof int[] )
                    return Arrays.toString( (int[]) argument );
                if( argument instanceof long[] )
                    return Arrays.toString( (long[]) argument );
                if( argument instanceof short[] )
                    return Arrays.toString( (short[]) argument );
                if( argument instanceof Object[] )
                {
                    StringBuilder s = new StringBuilder( "{" );
                    for( Object each : (Object[]) argument )
                    {
                        if( s.length() != 1 )
                            s.append( ", " );
                        s.append( toString( each ) );
                    }
                    return s.append( "}" ).toString();
                }
                throw new IllegalArgumentException( "Unknown array type: "
                        + argument.getClass().getCanonicalName() );
            }

            if( argument instanceof CharSequence )
                return "\"" + argument + "\"";

            return argument.toString();
        }
        catch( ExpectationFailure ex )
        {
            /* argument is a mock object which doesn't implement toString() */
            return "mock@" + Integer.toHexString( System.identityHashCode( argument ) );
        }
        finally
        {
            InvocationContext.setIncrementable( previousIncrementable );
        }
    }

}
